#include <LiquidCrystal.h>
#include <Button.h>
#include "Wire.h"

#include "Clock.h"
#include "Driver.h"
#include "Weather.h"
#include "serialcommand.h"

#define DS1307_I2C_ADDRESS 0x68
#define PCA9685_I2C_ADDRESS 0x5f

#define LIGHTNING_CHANCE 10
#define MAX_WHITE 80
#define MIN_WHITE 0.03
#define MAX_BLUE 80
#define MIN_BLUE 0.03

SerialCommand command;
LiquidCrystal lcd(12, 11, 10, 9, 8, 7, 6);
Button buttonFull = Button(2, PULLUP);
Button buttonLightning = Button(0, PULLUP);

Clock clock(DS1307_I2C_ADDRESS);
Driver driver(PCA9685_I2C_ADDRESS);
Weather weather(MAX_WHITE, MAX_BLUE);

uint8_t dayOfMonth = 40; // Invalid... Trigger a new day
uint8_t prevMinute = 0;
uint8_t fadeSpeed = 0;

bool overrideMode = false;

/////////////////////////////////////////////////////////////
// Section where we define the white-blue channel pairs
struct ChannelPair 
{
	uint8_t wChannel;
	unsigned int wCache;

	uint8_t bChannel;
	unsigned int bCache;

	uint8_t channelDelay;

	uint8_t fade;
};

#define MAX_CHANNEL_PAIRS 6
ChannelPair channels[MAX_CHANNEL_PAIRS] = 
{	
//   wCh, wCache, bCh, bCache, delay, fade
	{ 0,  0,      1,   0,      0,     0 },
	{ 4,  0,      7,   0,      0,     0 },
	{ 12, 0,      13,  0,      0,     0 },
	{ 14, 0,      15,  0,      0,     0 },
	{ 8,  0,      11,  0,      0,     0 },
	{ 10, 0,      9,   0,      0,     0 }
};

bool fullLight = false;
bool fullLightPushed = false;
bool noLightPushed = false;
bool levelsMode = false;

byte lcdWithLeadingZero(unsigned int number) 
{
  if (number < 10) 
  {
    lcd.print("0");
    return lcd.print(number,DEC) + 1;
  } 
  else 
  {
    return lcd.print(number,DEC);
  }
}

void lcdDateTime()
{
	lcdWithLeadingZero(2000 + clock.year());
	lcd.print("-");
	lcdWithLeadingZero(clock.month());
	lcd.print("-");
	lcdWithLeadingZero(clock.dayOfMonth());
	lcd.print(" ");
	
	lcdWithLeadingZero(clock.hour());
	lcd.print(":");
	lcdWithLeadingZero(clock.minute());
	lcd.print(":");
	lcdWithLeadingZero(clock.second());
	lcd.print(" ");
}

void setLEDDirect(byte channel, float whitePwmLevel, float bluePwmLevel) 
{
	whitePwmLevel = whitePwmLevel != 0 && whitePwmLevel < MIN_WHITE ? MIN_WHITE : whitePwmLevel;
	bluePwmLevel = bluePwmLevel != 0 && bluePwmLevel < MIN_BLUE ? MIN_BLUE : bluePwmLevel;

	channels[channel].wCache = whitePwmLevel == 0 ? 0 : ceil((whitePwmLevel * 0xfff) / 100.0);
	driver.setLevel(channels[channel].wChannel, 0, channels[channel].wCache);

	channels[channel].bCache = bluePwmLevel == 0 ? 0 : ceil((bluePwmLevel * 0xfff) / 100.0);
	driver.setLevel(channels[channel].bChannel, 0, channels[channel].bCache);
} 

void setLEDDelay(byte channel, float& whitePwmLevel, float& bluePwmLevel) 
{
	whitePwmLevel = whitePwmLevel != 0 && whitePwmLevel < MIN_WHITE ? MIN_WHITE : whitePwmLevel;
	bluePwmLevel = bluePwmLevel != 0 && bluePwmLevel < MIN_BLUE ? MIN_BLUE : bluePwmLevel;

	channels[channel].fade++;
	if (channels[channel].fade < fadeSpeed)
	{
		whitePwmLevel = (float)channels[channel].wCache / (float)0xfff * 100.0f;
		bluePwmLevel = (float)channels[channel].bCache / (float)0xfff * 100.0f;
		return;
	}

	unsigned int goal = whitePwmLevel == 0 ? 0 : ceil((whitePwmLevel * 0xfff) / 100.0);
	channels[channel].wCache += goal == channels[channel].wCache ? 0 : goal < channels[channel].wCache ? -1 : 1;
	driver.setLevel(channels[channel].wChannel, 0, channels[channel].wCache);

	goal = bluePwmLevel == 0 ? 0 : ceil((bluePwmLevel * 0xfff) / 100.0);
	channels[channel].bCache += goal == channels[channel].bCache ? 0 : goal < channels[channel].bCache ? -1 : 1;
	driver.setLevel(channels[channel].bChannel, 0, channels[channel].bCache);

	whitePwmLevel = (float)channels[channel].wCache / (float)0xfff * 100.0f;
	bluePwmLevel = (float)channels[channel].bCache / (float)0xfff * 100.0f;

	channels[channel].fade = 0;
} 

/******************************************************************************************
 * DO LIGHTNING
 *
 * Do lightning, flashing all the LEDs at full intensity in a lightning like pattern.
 *
 * Inspired by lightning code posted by Numlock10@ReefCentral
 * http://www.reefcentral.com/forums/showpost.php?p=17542851&postcount=206
 **/
void doLightning(float aWhiteLevel, float aBlueLevel) 
{
    byte numberOfFlashes = (byte)random(5)+1;
	byte maxLightLevel = (aBlueLevel < 20) ? (aBlueLevel * 2) + 3 : MAX_WHITE;

	for (byte var = 0; var < numberOfFlashes; var++)
	{
		// LEDs on for 50ms
		for (byte i=0; i<MAX_CHANNEL_PAIRS; i++)
			setLEDDirect(i, maxLightLevel, maxLightLevel);
		delay(50);
      
		// LED off for 50ms
		for (byte i=0; i<MAX_CHANNEL_PAIRS; i++)
			setLEDDirect(i, 0, 0);
		delay(50);
      
		// LED on for 50ms to 250ms
		for (byte i=0; i<MAX_CHANNEL_PAIRS; i++)
			setLEDDirect(i, maxLightLevel, maxLightLevel);
		delay(random(50,250));           
      
		// set the LED back to normal levels for 50ms to 1sec
		for (byte i=0; i<MAX_CHANNEL_PAIRS; i++)
			setLEDDirect(i, aWhiteLevel, aBlueLevel);
		delay(random(50,1000));            
    }
}

void lcdHeartbeat(float wLevel, float bLevel)
{
	lcd.print("W=");
	lcd.print(wLevel);
	lcd.print("%, B=");
	lcd.print(bLevel);
	lcd.print("%    ");
}

void updateDisplay(float wLevel, float bLevel)
{
	// print out date and time
	lcd.setCursor(0,0);
	lcdDateTime();
	
	// print out light levels 
	lcd.setCursor(0,1);
	lcdHeartbeat(wLevel, bLevel);
}

bool cmdHello()
{
	return true;
}

bool cmdOverride()
{
	char* arg = command.next();
	if (arg != NULL && atoi(arg) > 0) 
	{
		Serial.println("OVERRIDE-ON");
		overrideMode = true;
	}
	else
	{
		Serial.println("OVERRIDE-OFF");
		overrideMode = false;
	}

	return true;
}

bool cmdSetLED()
{
	if (overrideMode == false)
		return false;

	char* argLed = command.next();
	if (argLed == NULL || (atoi(argLed) < 0 || atoi(argLed) > 15))
	{
		Serial.println("INVALID LED CHANNEL");
		return false;
	}

	char* argLevel = command.next();
	if (argLevel == NULL || (atoi(argLevel) < 0 || atoi(argLevel) > 0xfff))
	{
		Serial.println("INVALID LED LEVEL");
		return false;
	}

	driver.setLevel(atoi(argLed), 0, atoi(argLevel));
	return true;
}

bool cmdLightning()
{
	if (overrideMode == false)
		return false;

	doLightning(0, 0);
	return true;
}

bool cmdSetClock()
{
	char* argYear = command.next();
	if (argYear == NULL || (atoi(argYear) < 0 || atoi(argYear) > 15))
	{
		Serial.println("INVALID YEAR");
		return false;
	}

	char* argMonth = command.next();
	if (argMonth == NULL || (atoi(argMonth) < 1 || atoi(argMonth) > 12))
	{
		Serial.println("INVALID MONTH");
		return false;
	}

	char* argDay = command.next();
	if (argDay == NULL || (atoi(argDay) < 1 || atoi(argDay) > 31))
	{
		Serial.println("INVALID DAY");
		return false;
	}

	char* argDayOfWeek = command.next();
	if (argDayOfWeek == NULL || (atoi(argDayOfWeek) < 1 || atoi(argDayOfWeek) > 7))
	{
		Serial.println("INVALID DAY OF WEEK");
		return false;
	}

	char* argHour = command.next();
	if (argHour == NULL || (atoi(argHour) < 0 || atoi(argHour) > 23))
	{
		Serial.println("INVALID HOUR");
		return false;
	}

	char* argMinute = command.next();
	if (argMinute == NULL || (atoi(argMinute) < 0 || atoi(argMinute) > 59))
	{
		Serial.println("INVALID MINUTE");
		return false;
	}

	char* argSecond = command.next();
	if (argSecond == NULL || (atoi(argSecond) < 0 || atoi(argSecond) > 59))
	{
		Serial.println("INVALID SECOND");
		return false;
	}

	// Set time
	clock.set(atoi(argYear), atoi(argMonth), atoi(argDay), 
		atoi(argHour), atoi(argMinute), atoi(argSecond), atoi(argDayOfWeek)); 

	return true;
}

bool cmdSetWeather()
{
	char* argWeather = command.next();
	if (argWeather == NULL || (atoi(argWeather) < 0 || atoi(argWeather) > 8))
	{
		Serial.println("INVALID WEATHER");
		return false;
	}

	int okta = atoi(argWeather);
	switch (okta)
	{
	case 0:
		// No clouds, nothing to do....
		Serial.println("clear");
		break;
      
	case 1:
    case 2: // these days will be "short cloud + space"
		Serial.println("partly cloudy");
		break;
      
    case 3:
    case 4: // these days will be "short cloud + space + long cloud + space"
		Serial.println("mostly cloudy");
		break;
      
    case 5: // Morning of short clouds spaced as an okta 2 day, followed by one thunderstorm in the afternoon;
		Serial.println("partly cloudy. afternoon thunderstorm");
		break;
      
    case 6: // Morning of long clouds spaced as an okta 4 day, followed by one thunderstorm in the afternoon;
		Serial.println("mostly cloudy. afternoon thunderstorm");
		break;
      
    case 7: // these days will be "long cloud + space"
		Serial.println("very cloudy");
		break;
        
    case 8: // heavy thunderstorm day... one after the other with a short space between them
		Serial.println("heavy thunderstorms");
		break;
	}

	weather.changeOkta(okta);
	return true;
}

bool cmdGetCurve()
{
	weather.dumpCurve();
	return true;
}

void setup()
{
	Serial.begin(9600);
	Wire.begin();

	// Initialize i2c modules
	driver.begin();
	clock.begin();

	// Start the LCD
	pinMode(4, OUTPUT);
	digitalWrite(4, HIGH); // turn backlight on. Replace 'HIGH' with 'LOW' to turn it off.
	lcd.begin(20,2);              // columns, rows.  use 16,2 for a 16x2 LCD, etc.
	lcd.clear();                  // start with a blank screen

	randomSeed(analogRead(0));

	// Register commands
	//command.addCommand("getcurve", cmdGetCurve);
	command.addCommand("hello", cmdHello);
	command.addCommand("override", cmdOverride);
	command.addCommand("setled", cmdSetLED);
	command.addCommand("lightning", cmdLightning);
	command.addCommand("setclock", cmdSetClock);
	command.addCommand("setweather", cmdSetWeather);
}


void loop()
{
	clock.beat();
	command.readSerial();

	if (overrideMode)
	{
		// override means that the leds are controlled by
		// external PC
		for (int r=0; r<=1; r++)
		{
			lcd.setCursor(0,r);
			lcd.print("***** OVERRIDE ***** ");
		}
		
		// Nothing more to be done. 
		return;
	}


	bool fullLightChanged = false;
	if (buttonFull.isPressed())
	{
		if (!fullLight)
			fullLightPushed = true;
		else
			noLightPushed = true;
	}
	else
	{
		if (fullLightPushed && !fullLight)
		{
			//clock.set(12, 07, 11, 22, 48, 00, 3);

			fullLight = true;
			fullLightChanged = true;
		}

		if (noLightPushed)
		{
			fullLight = false;
			fullLightChanged = true;
		}

		fullLightPushed = false;
		noLightPushed = false;
	}

	// If the day changed, plan the new day
	if (dayOfMonth != clock.dayOfMonth()) 
	{
		dayOfMonth = clock.dayOfMonth();
		weather.planNewDay(clock.month(), clock.dayOfMonth());
	}

    unsigned int now = clock.now();

	bool minuteChanged = false;
	if (prevMinute != clock.minute()) 
	{
		prevMinute = clock.minute();
		minuteChanged = true;
	}

	bool inThunder = false;
	float wLevel = 0;
	float bLevel = 0;
	
	// Loop through the LED channel pairs getting their light levels
	for (byte i=0; i<MAX_CHANNEL_PAIRS; i++) 
	{
		// Protection against unsigned int roll backwards
		unsigned int chanDelay = channels[i].channelDelay;
		if (chanDelay > now)
			chanDelay = now;
    
		bool channelInThunder;
		weather.getLevel(now - chanDelay, channelInThunder, wLevel, bLevel);

		if (fullLight)
		{
			wLevel = MAX_WHITE;
			bLevel = MAX_BLUE;
		}

		if (fullLightChanged)
			setLEDDirect(i, wLevel, bLevel);
		else
			setLEDDelay(i, wLevel, bLevel);

		if (channels[i].channelDelay == 0)
			inThunder = channelInThunder;
	}

	if (buttonLightning.isPressed())
		doLightning(wLevel, bLevel);

	// If in Thunderstorm, 10% possible lighning every minute
	if ((inThunder) && (minuteChanged)) 
	{
		byte randNumber = (byte) random(0, 100);
		if (randNumber <= LIGHTNING_CHANCE)  
			doLightning(wLevel, bLevel);
	}

	weather.planNextCloudBatch(now);

	updateDisplay(wLevel, bLevel);
}
